package com.imis.base.aspect;

import com.imis.base.annotation.RepeatOperationLock;
import com.imis.base.constant.CacheConstant;
import com.imis.base.constant.CommonConstant;
import com.imis.base.constant.HttpHeadersConstants;
import com.imis.base.constant.enums.RepeatOperationLockEnum;
import com.imis.base.constant.enums.SysTipEnum;
import com.imis.base.util.ConvertUtils;
import com.imis.base.util.IPUtils;
import com.imis.base.util.RedisUtil;
import com.imis.base.util.SpringContextUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.assertj.core.api.Assertions;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;

/**
 * <p>
 * RepeatOperationLockAspect<br>
 * 防止高并发重复请求 拦截器
 * </p>
 *
 * @author XinLau
 * @version 1.0
 * @since 2020年08月20日 14:58
 */
@Slf4j
@Aspect
@Component
public class RepeatOperationLockAspect extends BaseAspect {

    /**
     * Redis 工具类
     */
    @Resource
    private RedisUtil redisUtil;

    /**
     * 自定义切点
     *
     * @author XinLau
     * @creed The only constant is change ! ! !
     * @since 2020/7/31 18:00
     */
    @Pointcut("@annotation(com.imis.base.annotation.RepeatOperationLock)")
    public void pointCut() {
    }

    /**
     * 上锁
     *
     * @param joinPoint - JoinPoint
     * @author XinLau
     * @creed The only constant is change ! ! !
     * @since 2020/3/25 09:35
     */
    @Before("pointCut()")
    public void onLock(JoinPoint joinPoint) {
        log.debug("防止方法重复调用--->上锁：joinPoint:{}", joinPoint);
        // 1.获取被拦截方法对象
        Method method = getMethod(joinPoint);
        // 2.获取 Annotation
        RepeatOperationLock onRepeatOperationLock = method.getAnnotation(RepeatOperationLock.class);
        // 3.获取锁标识
        String identification = getLockIdentification(joinPoint, method, onRepeatOperationLock);
        // 4.设置缓存
        Long flag = redisUtil.incr(identification, 1);
        Assertions.assertThat(flag).as(SysTipEnum.SC_ERROR_REPEAT_OPERATION.toString(), identification).isEqualTo(1L);
        // 5.获取并设置过期时间
        long timeOut = onRepeatOperationLock.timeOut();
        redisUtil.expire(identification, timeOut);
    }

    /**
     * 解锁
     *
     * @param joinPoint - JoinPoint
     * @author XinLau
     * @creed The only constant is change ! ! !
     * @since 2020/3/25 09:35
     */
    @After("pointCut()")
    public void unLock(JoinPoint joinPoint) {
        log.debug("防止方法重复调用--->解锁：joinPoint:{}", joinPoint);
        // 1.获取被拦截方法对象
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        // 2.获取 Annotation
        RepeatOperationLock unRepeatOperationLock = method.getAnnotation(RepeatOperationLock.class);
        // 3.判断是否自动解锁
        /**
         * <ul>
         *  <li>1、false：执行完方法立即解锁</li>
         *  <li>2、true：等待锁时长结束解锁</li>
         * </ul>
         */
        if (!unRepeatOperationLock.automaticUnlocking()) {
            // 3.获取锁标识
            String identification = getLockIdentification(joinPoint, method, unRepeatOperationLock);
            // 3.2删除缓存
            redisUtil.del(identification);
        }
    }

    /**
     * 获取锁标识
     *
     * @param joinPoint           - JoinPoint
     * @param method              - 方法
     * @param repeatOperationLock - 锁注解信息
     * @return String -
     * @author XinLau
     * @creed The only constant is change ! ! !
     * @since 2020/8/21 9:40
     */
    private String getLockIdentification(JoinPoint joinPoint, Method method, RepeatOperationLock repeatOperationLock) {
        // 1.获取参数生成标识
        String lockIdentification = repeatOperationLock.lockIdentification();
        lockIdentification = ConvertUtils.isEmpty(lockIdentification) ? CacheConstant.PREFIX_LOCK_IDENTIFICATION : generateStringBySpEL2(repeatOperationLock.lockIdentification(), joinPoint);
        // 2.获取锁类型
        RepeatOperationLockEnum repeatOperationLockEnum = repeatOperationLock.type();
        // 3.根据锁类型生成标识前缀
        String prefix = CommonConstant.EMPTY;
        if (RepeatOperationLockEnum.PARAMETER == repeatOperationLockEnum) {
            prefix = lockIdentification;
        } else {
            prefix = getPrefixByLockType(repeatOperationLockEnum);
        }
        String identification = prefix + CommonConstant.COLON + getClassName(joinPoint) + CommonConstant.COLON + method.getName() + CommonConstant.COLON + lockIdentification;
        log.debug("防止方法重复调用--->锁标识：identification:{}", identification);
        return identification;
    }

    /**
     * 根据锁类型生成标识前缀
     *
     * @param repeatOperationLockEnum - 重复操作锁定类型
     * @return String - 标识前缀
     * @author XinLau
     * @creed The only constant is change ! ! !
     * @since 2020/8/21 9:35
     */
    private String getPrefixByLockType(RepeatOperationLockEnum repeatOperationLockEnum) {
        // 获取Token
        HttpServletRequest httpServletRequest = SpringContextUtils.getHttpServletRequest();
        String prefix = "";
        if (RepeatOperationLockEnum.IP == repeatOperationLockEnum) {
            // 获取IP地址
            prefix = IPUtils.getClientIpAddress(httpServletRequest);
        } else if (RepeatOperationLockEnum.TOKEN == repeatOperationLockEnum) {
            prefix = httpServletRequest.getHeader(HttpHeadersConstants.X_ACCESS_TOKEN);
            Assertions.assertThat(prefix).as(SysTipEnum.SC_ERROR_NO_TOKEN.toString()).isBlank().isEmpty();
        }
        return prefix;
    }

}
